using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Markup;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Utils;

namespace ICSharpCode.AvalonEdit
{
    /// <summary>
    /// The text editor control.
    /// Contains a scrollable TextArea.
    /// </summary>
    [Localizability(LocalizationCategory.Text), ContentProperty("Text")]
    public class TextEditor : Control, ITextEditorComponent, IWeakEventListener, INotifyPropertyChanged
    {
        #region Constructors

        static TextEditor()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof (TextEditor), new FrameworkPropertyMetadata(typeof (TextEditor)));
            FocusableProperty.OverrideMetadata(typeof (TextEditor), new FrameworkPropertyMetadata(Boxes.True));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TextEditor"/> class.
        /// </summary>
        public TextEditor()
            : this(new TextArea())
        {
        }

        private TextEditor(TextArea textArea)
        {
            TextArea = textArea ?? new TextArea();
            TextArea.TextView.Services.AddService(typeof (TextEditor), this);
            Options = TextArea.Options;
            Document = new TextDocument(); 
            TextArea.PropertyChanged += (sender, args) =>
                {
                    if (args.PropertyName != "Selection") return;
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectionStart"));
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectionEnd"));
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectionLength"));
                };
        }

        #endregion

        #region ITextEditorComponent Members

        object IServiceProvider.GetService(Type serviceType)
        {
            return TextArea.GetService(serviceType);
        }

        #endregion

        /// <inheritdoc/>
        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return new TextEditorAutomationPeer(this);
        }

        /// Forward focus to TextArea.
        /// <inheritdoc/>
        protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
        {
            base.OnGotKeyboardFocus(e);
            if (TextArea.IsKeyboardFocusWithin) return;
            Keyboard.Focus(TextArea);
            e.Handled = true;
        }

        /// <summary>
        /// Gets the text view position from a point inside the editor.
        /// </summary>
        /// <param name="point">The position, relative to top left
        /// corner of TextEditor control</param>
        /// <returns>The text view position, or null if the point is outside the document.</returns>
        public TextViewPosition? GetPositionFromPoint(Point point)
        {
            if (Document == null) return null;
            var textView = TextArea.TextView;
            return textView.GetPosition(TranslatePoint(point, textView) + textView.ScrollOffset);
        }

        /// <summary>
        /// Scrolls to the specified line/column.
        /// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior).
        /// </summary>
        public void ScrollTo(int line, int column = -1)
        {
            const double minimumScrollPercentage = 0.3;

            if (ScrollViewer == null) return;
            var p = TextArea.TextView.GetVisualPosition(new TextViewPosition(line, Math.Max(1, column)), VisualYPosition.LineMiddle);
            var verticalPos = p.Y - ScrollViewer.ViewportHeight/2;
            if (Math.Abs(verticalPos - ScrollViewer.VerticalOffset) >
                minimumScrollPercentage*ScrollViewer.ViewportHeight)
                ScrollViewer.ScrollToVerticalOffset(Math.Max(0, verticalPos));

            if (column <= 0) return;
            if (p.X > ScrollViewer.ViewportWidth - Caret.MinimumDistanceToViewBorder*2)
            {
                var horizontalPos = Math.Max(0, p.X - ScrollViewer.ViewportWidth/2);
                if (Math.Abs(horizontalPos - ScrollViewer.HorizontalOffset) >
                    minimumScrollPercentage*ScrollViewer.ViewportWidth)
                    ScrollViewer.ScrollToHorizontalOffset(horizontalPos);
            }
            else ScrollViewer.ScrollToHorizontalOffset(0);
        }

        #region Document property

        /// <summary>
        /// Document property.
        /// </summary>
        public static readonly DependencyProperty DocumentProperty
            = TextView.DocumentProperty.AddOwner(
                typeof (TextEditor), new FrameworkPropertyMetadata(OnDocumentChanged));

        /// <summary>
        /// Gets/Sets the document displayed by the text editor.
        /// This is a dependency property.
        /// </summary>
        public TextDocument Document
        {
            get { return (TextDocument) GetValue(DocumentProperty); }
            set { SetValue(DocumentProperty, value); }
        }

        /// <summary>
        /// Occurs when the document property has changed.
        /// </summary>
        public event EventHandler DocumentChanged = (sender, args) => { };

        /// <summary>
        /// Raises the <see cref="DocumentChanged"/> event.
        /// </summary>
        protected virtual void OnDocumentChanged(EventArgs e)
        {
            DocumentChanged(this, e);
        }

        private static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
        {
            ((TextEditor) dp).OnDocumentChanged((TextDocument) e.OldValue, (TextDocument) e.NewValue);
        }

        private void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
        {
            if (oldValue != null)
            {
                TextDocumentWeakEventManager.TextChanged.RemoveListener(oldValue, this);
                PropertyChangedEventManager.RemoveListener(oldValue.UndoStack, this, "IsOriginalFile");
            }

            TextArea.Document = newValue;
            if (newValue != null)
            {
                TextDocumentWeakEventManager.TextChanged.AddListener(newValue, this);
                PropertyChangedEventManager.AddListener(newValue.UndoStack, this, "IsOriginalFile");
            }

            OnDocumentChanged(EventArgs.Empty);
            OnTextChanged(EventArgs.Empty);
        }

        #endregion

        #region Options property

        /// <summary>
        /// Options property.
        /// </summary>
        public static readonly DependencyProperty OptionsProperty
            = TextView.OptionsProperty.AddOwner(typeof (TextEditor), new FrameworkPropertyMetadata(OnOptionsChanged));

        #region ITextEditorComponent Members

        /// <summary>
        /// Gets/Sets the options currently used by the text editor.
        /// </summary>
        public TextEditorOptions Options
        {
            get { return (TextEditorOptions) GetValue(OptionsProperty); }
            set { SetValue(OptionsProperty, value); }
        }

        /// <summary>
        /// Occurs when a text editor option has changed.
        /// </summary>
        public event PropertyChangedEventHandler OptionChanged;

        #endregion

        #region IWeakEventListener Members

        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            return ReceiveWeakEvent(managerType, sender, e);
        }

        #endregion

        /// <summary>
        /// Raises the <see cref="OptionChanged"/> event.
        /// </summary>
        protected virtual void OnOptionChanged(PropertyChangedEventArgs e)
        {
            if (OptionChanged != null)
            {
                OptionChanged(this, e);
            }
        }

        private static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
        {
            ((TextEditor) dp).OnOptionsChanged((TextEditorOptions) e.OldValue, (TextEditorOptions) e.NewValue);
        }

        private void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue)
        {
            if (oldValue != null)
            {
                PropertyChangedWeakEventManager.RemoveListener(oldValue, this);
            }
            TextArea.Options = newValue;
            if (newValue != null)
            {
                PropertyChangedWeakEventManager.AddListener(newValue, this);
            }
            OnOptionChanged(new PropertyChangedEventArgs(null));
        }

        /// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
        protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            if (managerType == typeof (PropertyChangedWeakEventManager))
            {
                OnOptionChanged((PropertyChangedEventArgs) e);
                return true;
            }
            if (managerType == typeof (TextDocumentWeakEventManager.TextChanged))
            {
                OnTextChanged(e);
                return true;
            }
            if (managerType == typeof (PropertyChangedEventManager))
            {
                return HandleIsOriginalChanged((PropertyChangedEventArgs) e);
            }
            return false;
        }

        #endregion

        #region Text property

        /// <summary>
        /// Gets/Sets the text of the current document.
        /// </summary>
        [Localizability(LocalizationCategory.Text), DefaultValue("")]
        public string Text
        {
            get
            {
                var document = Document;
                return document != null ? document.Text : string.Empty;
            }
            set
            {
                var document = GetDocument();
                document.Text = value ?? string.Empty;
                // after replacing the full text, the caret is positioned at the end of the document
                // - reset it to the beginning.
                TextArea.Caret.Offset = 0;
                document.UndoStack.ClearAll();
            }
        }

        private TextDocument GetDocument()
        {
            var document = Document;
            if (document == null)
                throw ThrowUtil.NoDocumentAssigned();
            return document;
        }

        /// <summary>
        /// Occurs when the Text property changes.
        /// </summary>
        public event EventHandler TextChanged = (sender, args) => { };

        /// <summary>
        /// Raises the <see cref="TextChanged"/> event.
        /// </summary>
        protected virtual void OnTextChanged(EventArgs e)
        {
            TextChanged(this, e);
        }

        #endregion

        #region TextArea / ScrollViewer properties

        /// <summary>
        /// Gets the text area.
        /// </summary>
        public TextArea TextArea { get; private set; }

        /// <summary>
        /// Gets the scroll viewer used by the text editor.
        /// This property can return null if the template has not been applied / does not contain a scroll viewer.
        /// </summary>
        internal ScrollViewer ScrollViewer { get; private set; }

        /// <summary>
        /// Is called after the template was applied.
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            ScrollViewer = (ScrollViewer) Template.FindName("PART_ScrollViewer", this);
            TemplateApplied();
        }

        /// <summary>
        /// 
        /// </summary>
        public Action TemplateApplied = () => { };

        private bool CanExecute(RoutedCommand command)
        {
            return TextArea != null && command.CanExecute(null, TextArea);
        }

        private void Execute(RoutedCommand command)
        {
            command.Execute(null, TextArea);
        }

        #endregion

        #region WordWrap

        /// <summary>
        /// Word wrap dependency property.
        /// </summary>
        public static readonly DependencyProperty WordWrapProperty =
            DependencyProperty.Register("WordWrap", typeof (bool), typeof (TextEditor),
                                        new FrameworkPropertyMetadata(Boxes.False));

        /// <summary>
        /// Specifies whether the text editor uses word wrapping.
        /// </summary>
        public bool WordWrap
        {
            get { return (bool) GetValue(WordWrapProperty); }
            set { SetValue(WordWrapProperty, Boxes.Box(value)); }
        }

        #endregion

        #region IsReadOnly

        /// <summary>
        /// IsReadOnly dependency property.
        /// </summary>
        public static readonly DependencyProperty IsReadOnlyProperty =
            DependencyProperty.Register("IsReadOnly", typeof (bool), typeof (TextEditor),
                                        new FrameworkPropertyMetadata(Boxes.False, OnIsReadOnlyChanged));

        /// <summary>
        /// Specifies whether the user can change the text editor content.
        /// Setting this property will replace the
        /// <see cref="Editing.TextArea.ReadOnlySectionProvider">TextArea.ReadOnlySectionProvider</see>.
        /// </summary>
        public bool IsReadOnly
        {
            get { return (bool) GetValue(IsReadOnlyProperty); }
            set { SetValue(IsReadOnlyProperty, Boxes.Box(value)); }
        }

        private static void OnIsReadOnlyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var editor = d as TextEditor;
            if (editor != null)
            {
                if ((bool) e.NewValue)
                    editor.TextArea.ReadOnlySectionProvider = ReadOnlyDocument.Instance;
                else
                    editor.TextArea.ReadOnlySectionProvider = NoReadOnlySections.Instance;

                var peer = UIElementAutomationPeer.FromElement(editor) as TextEditorAutomationPeer;
                if (peer != null)
                {
                    peer.RaiseIsReadOnlyChanged((bool) e.OldValue, (bool) e.NewValue);
                }
            }
        }

        #endregion

        #region IsModified

        /// <summary>
        /// Dependency property for <see cref="IsModified"/>
        /// </summary>
        public static readonly DependencyProperty IsModifiedProperty =
            DependencyProperty.Register("IsModified", typeof (bool), typeof (TextEditor),
                                        new FrameworkPropertyMetadata(Boxes.False, OnIsModifiedChanged));

        /// <summary>
        /// Gets/Sets the 'modified' flag.
        /// </summary>
        public bool IsModified
        {
            get { return (bool) GetValue(IsModifiedProperty); }
            set { SetValue(IsModifiedProperty, Boxes.Box(value)); }
        }

        private static void OnIsModifiedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var editor = d as TextEditor;
            if (editor != null)
            {
                TextDocument document = editor.Document;
                if (document != null)
                {
                    UndoStack undoStack = document.UndoStack;
                    if ((bool) e.NewValue)
                    {
                        if (undoStack.IsOriginalFile)
                            undoStack.DiscardOriginalFileMarker();
                    }
                    else
                    {
                        undoStack.MarkAsOriginalFile();
                    }
                }
            }
        }

        private bool HandleIsOriginalChanged(PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "IsOriginalFile")
            {
                TextDocument document = Document;
                if (document != null)
                {
                    IsModified = !document.UndoStack.IsOriginalFile;
                }
                return true;
            }
            return false;
        }

        #endregion

        #region ShowLineNumbers

        /// <summary>
        /// IsReadOnly dependency property.
        /// </summary>
        public static readonly DependencyProperty ShowLineNumbersProperty =
            DependencyProperty.Register("ShowLineNumbers", typeof (bool), typeof (TextEditor),
                                        new FrameworkPropertyMetadata(Boxes.False, OnShowLineNumbersChanged));

        /// <summary>
        /// Specifies whether line numbers are shown on the left to the text view.
        /// </summary>
        public bool ShowLineNumbers
        {
            get { return (bool) GetValue(ShowLineNumbersProperty); }
            set { SetValue(ShowLineNumbersProperty, Boxes.Box(value)); }
        }

        private static void OnShowLineNumbersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var editor = (TextEditor) d;
            var leftMargins = editor.TextArea.LeftMargins;
            if ((bool) e.NewValue)
            {
                leftMargins.Insert(0, new LineNumberMargin());
                leftMargins.Insert(1, DottedLineMargin.Create(editor.TextArea));
            }
            else
            {
                for (var i = 0; i < leftMargins.Count; i++)
                {
                    if (leftMargins[i] is LineNumberMargin)
                    {
                        leftMargins.RemoveAt(i);
                        if (i < leftMargins.Count && DottedLineMargin.IsDottedLineMargin(leftMargins[i]))
                        {
                            leftMargins.RemoveAt(i);
                        }
                        break;
                    }
                }
            }
        }

        #endregion

        #region TextBoxBase-like methods

        /// <summary>
        /// Gets if the most recent undone command can be redone.
        /// </summary>
        public bool CanRedo
        {
            get { return CanExecute(ApplicationCommands.Redo); }
        }

        /// <summary>
        /// Gets if the most recent command can be undone.
        /// </summary>
        public bool CanUndo
        {
            get { return CanExecute(ApplicationCommands.Undo); }
        }

        /// <summary>
        /// Gets the vertical size of the document.
        /// </summary>
        public double ExtentHeight
        {
            get { return ScrollViewer != null ? ScrollViewer.ExtentHeight : 0; }
        }

        /// <summary>
        /// Gets the horizontal size of the current document region.
        /// </summary>
        public double ExtentWidth
        {
            get { return ScrollViewer != null ? ScrollViewer.ExtentWidth : 0; }
        }

        /// <summary>
        /// Gets the horizontal size of the viewport.
        /// </summary>
        public double ViewportHeight
        {
            get { return ScrollViewer != null ? ScrollViewer.ViewportHeight : 0; }
        }

        /// <summary>
        /// Gets the horizontal size of the viewport.
        /// </summary>
        public double ViewportWidth
        {
            get { return ScrollViewer != null ? ScrollViewer.ViewportWidth : 0; }
        }

        /// <summary>
        /// Gets the vertical scroll position.
        /// </summary>
        public double VerticalOffset
        {
            get { return ScrollViewer != null ? ScrollViewer.VerticalOffset : 0; }
        }

        /// <summary>
        /// Gets the horizontal scroll position.
        /// </summary>
        public double HorizontalOffset
        {
            get { return ScrollViewer != null ? ScrollViewer.HorizontalOffset : 0; }
        }

        /// <summary>
        /// Appends text to the end of the document.
        /// </summary>
        public void AppendText(string textData)
        {
            TextDocument document = GetDocument();
            document.Insert(document.TextLength, textData);
        }

        /// <summary>
        /// Begins a group of document changes.
        /// </summary>
        public void BeginChange()
        {
            GetDocument().BeginUpdate();
        }

        /// <summary>
        /// Copies the current selection to the clipboard.
        /// </summary>
        public void Copy()
        {
            Execute(ApplicationCommands.Copy);
        }

        /// <summary>
        /// Removes the current selection and copies it to the clipboard.
        /// </summary>
        public void Cut()
        {
            Execute(ApplicationCommands.Cut);
        }

        /// <summary>
        /// Begins a group of document changes and returns an object that ends the group of document
        /// changes when it is disposed.
        /// </summary>
        public IDisposable DeclareChangeBlock()
        {
            return GetDocument().RunUpdate();
        }

        /// <summary>
        /// Ends the current group of document changes.
        /// </summary>
        public void EndChange()
        {
            GetDocument().EndUpdate();
        }

        /// <summary>
        /// Scrolls one line down.
        /// </summary>
        public void LineDown()
        {
            if (ScrollViewer != null)
                ScrollViewer.LineDown();
        }

        /// <summary>
        /// Scrolls to the left.
        /// </summary>
        public void LineLeft()
        {
            if (ScrollViewer != null)
                ScrollViewer.LineLeft();
        }

        /// <summary>
        /// Scrolls to the right.
        /// </summary>
        public void LineRight()
        {
            if (ScrollViewer != null)
                ScrollViewer.LineRight();
        }

        /// <summary>
        /// Scrolls one line up.
        /// </summary>
        public void LineUp()
        {
            if (ScrollViewer != null)
                ScrollViewer.LineUp();
        }

        /// <summary>
        /// Scrolls one page down.
        /// </summary>
        public void PageDown()
        {
            if (ScrollViewer != null)
                ScrollViewer.PageDown();
        }

        /// <summary>
        /// Scrolls one page up.
        /// </summary>
        public void PageUp()
        {
            if (ScrollViewer != null)
                ScrollViewer.PageUp();
        }

        /// <summary>
        /// Scrolls one page left.
        /// </summary>
        public void PageLeft()
        {
            if (ScrollViewer != null)
                ScrollViewer.PageLeft();
        }

        /// <summary>
        /// Scrolls one page right.
        /// </summary>
        public void PageRight()
        {
            if (ScrollViewer != null)
                ScrollViewer.PageRight();
        }

        /// <summary>
        /// Pastes the clipboard content.
        /// </summary>
        public void Paste()
        {
            Execute(ApplicationCommands.Paste);
        }

        /// <summary>
        /// Redoes the most recent undone command.
        /// </summary>
        /// <returns>True is the redo operation was successful, false is the redo stack is empty.</returns>
        public bool Redo()
        {
            if (CanExecute(ApplicationCommands.Redo))
            {
                Execute(ApplicationCommands.Redo);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Scrolls to the end of the document.
        /// </summary>
        public void ScrollToEnd()
        {
            if (ScrollViewer != null)
                ScrollViewer.ScrollToEnd();
        }

        /// <summary>
        /// Scrolls to the start of the document.
        /// </summary>
        public void ScrollToHome()
        {
            if (ScrollViewer != null)
                ScrollViewer.ScrollToHome();
        }

        /// <summary>
        /// Scrolls to the specified position in the document.
        /// </summary>
        public void ScrollToHorizontalOffset(double offset)
        {
            if (ScrollViewer != null)
                ScrollViewer.ScrollToHorizontalOffset(offset);
        }

        /// <summary>
        /// Scrolls to the specified position in the document.
        /// </summary>
        public void ScrollToVerticalOffset(double offset)
        {
            if (ScrollViewer != null)
                ScrollViewer.ScrollToVerticalOffset(offset);
        }

        /// <summary>
        /// Selects the entire text.
        /// </summary>
        public void SelectAll()
        {
            Execute(ApplicationCommands.SelectAll);
        }

        /// <summary>
        /// Undoes the most recent command.
        /// </summary>
        /// <returns>True is the undo operation was successful, false is the undo stack is empty.</returns>
        public bool Undo()
        {
            if (CanExecute(ApplicationCommands.Undo))
            {
                Execute(ApplicationCommands.Undo);
                return true;
            }
            return false;
        }

        #endregion

        #region TextBox methods

        /// <summary>
        /// Gets/Sets the selected text.
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public string SelectedText
        {
            get
            {
                // We'll get the text from the whole surrounding segment.
                // This is done to ensure that SelectedText.Length == SelectionLength.
                return TextArea.Document != null && !TextArea.Selection.IsEmpty
                           ? TextArea.Document.GetText(TextArea.Selection.SurroundingSegment)
                           : string.Empty;
            }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");
                if (TextArea == null || TextArea.Document == null) return;
                var offset = SelectionStart;
                var length = SelectionLength;
                TextArea.Document.Replace(offset, length, value);
                // keep inserted text selected
                TextArea.Selection = new SimpleSelection(offset, offset + value.Length);
            }
        }

        /// <summary>
        /// Gets/sets the start position of the selection.
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int SelectionStart
        {
            get { return TextArea.Selection.IsEmpty ? TextArea.Caret.Offset : TextArea.Selection.SurroundingSegment.Offset; }
            set { Select(value, SelectionLength); }
        }

        /// <summary>
        /// Gets/sets the end position of the selection.
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int SelectionEnd
        {
            get { return SelectionStart + SelectionLength; }
            set { Select(value - SelectionLength, SelectionLength); }
        }

        /// <summary>
        /// Gets/sets the length of the selection.
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int SelectionLength
        {
            get { return !TextArea.Selection.IsEmpty ? TextArea.Selection.SurroundingSegment.Length : 0; }
            set { Select(SelectionStart, value); }
        }

        /// <summary>
        /// Gets the number of lines in the document.
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int LineCount
        {
            get
            {
                var document = Document;
                return document != null ? document.LineCount : 1;
            }
        }

        /// <summary>
        /// Selects the specified text section.
        /// </summary>
        public void Select(int start, int length)
        {
            var documentLength = Document != null ? Document.TextLength : 0;
            if (start < 0 || start > documentLength)
                throw new ArgumentOutOfRangeException("start", start, "Value must be between 0 and " + documentLength);
            if (length < 0 || start + length > documentLength)
                throw new ArgumentOutOfRangeException("length", length,
                                                      "Value must be between 0 and " + (documentLength - length));
            TextArea.Selection = new SimpleSelection(start, start + length);
            TextArea.Caret.Offset = start + length;
        }

        #endregion

        #region Loading from stream

        /// <summary>
        /// Gets/sets the encoding used when the file is saved.
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public static readonly
            DependencyProperty EncodingProperty =
                DependencyProperty.Register("Encoding",
                                            typeof (Encoding),
                                            typeof (TextEditor),
                                            new PropertyMetadata(Encoding.UTF8));

        /// <summary>
        /// Gets or sets the encoding.
        /// </summary>
        /// <value>
        /// The encoding.
        /// </value>
        public Encoding Encoding
        {
            get { return (Encoding)GetValue(EncodingProperty); }
            set { SetValue(EncodingProperty, value); }
        }

        /// <summary>
        /// Loads the text from the stream, auto-detecting the encoding.
        /// </summary>
        /// <remarks>
        /// This method sets <see cref="IsModified"/> to false.
        /// </remarks>
        public void Load(Stream stream, Encoding encoding = null)
        {
            using (var reader =
                encoding == null
                    ? FileReader.OpenStream(stream, Encoding)
                    : new StreamReader(stream, encoding))
            {
                Text = reader.ReadToEnd();
                Encoding = reader.CurrentEncoding;
            }

            IsModified = false;
        }

        /// <summary>
        /// Loads the text from the stream, auto-detecting the encoding.
        /// </summary>
        public void Load(string fileName, Encoding encoding = null)
        {
            using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
                Load(stream, encoding);
        }

        /// <summary>
        /// Saves the text to the stream.
        /// </summary>
        /// <remarks>
        /// This method sets <see cref="IsModified"/> to false.
        /// </remarks>
        public void Save(Stream stream)
        {
            var writer = new StreamWriter(stream, Equals(Encoding, Encoding.ASCII) ? Encoding.Default : Encoding);
            writer.Write(Text);
            writer.Flush(); // do not close the stream
            IsModified = false;
        }

        /// <summary>
        /// Saves the text to the file.
        /// </summary>
        public void Save(string fileName)
        {
            using (var stream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
                Save(stream);
        }

        #endregion

        #region MouseHover events

        /// <summary>
        /// The PreviewMouseHover event.
        /// </summary>
        public static readonly RoutedEvent PreviewMouseHoverEvent =
            TextView.PreviewMouseHoverEvent.AddOwner(typeof (TextEditor));

        /// <summary>
        /// The MouseHover event.
        /// </summary>
        public static readonly RoutedEvent MouseHoverEvent =
            TextView.MouseHoverEvent.AddOwner(typeof (TextEditor));


        /// <summary>
        /// The PreviewMouseHoverStopped event.
        /// </summary>
        public static readonly RoutedEvent PreviewMouseHoverStoppedEvent =
            TextView.PreviewMouseHoverStoppedEvent.AddOwner(typeof (TextEditor));

        /// <summary>
        /// The MouseHoverStopped event.
        /// </summary>
        public static readonly RoutedEvent MouseHoverStoppedEvent =
            TextView.MouseHoverStoppedEvent.AddOwner(typeof (TextEditor));


        /// <summary>
        /// Occurs when the mouse has hovered over a fixed location for some time.
        /// </summary>
        public event MouseEventHandler PreviewMouseHover
        {
            add { AddHandler(PreviewMouseHoverEvent, value); }
            remove { RemoveHandler(PreviewMouseHoverEvent, value); }
        }

        /// <summary>
        /// Occurs when the mouse has hovered over a fixed location for some time.
        /// </summary>
        public event MouseEventHandler MouseHover
        {
            add { AddHandler(MouseHoverEvent, value); }
            remove { RemoveHandler(MouseHoverEvent, value); }
        }

        /// <summary>
        /// Occurs when the mouse had previously hovered but now started moving again.
        /// </summary>
        public event MouseEventHandler PreviewMouseHoverStopped
        {
            add { AddHandler(PreviewMouseHoverStoppedEvent, value); }
            remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); }
        }

        /// <summary>
        /// Occurs when the mouse had previously hovered but now started moving again.
        /// </summary>
        public event MouseEventHandler MouseHoverStopped
        {
            add { AddHandler(MouseHoverStoppedEvent, value); }
            remove { RemoveHandler(MouseHoverStoppedEvent, value); }
        }

        #endregion

        public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { };
    }
}